package com.devyk.av.rtmp.library.camera.renderer


import android.content.Context
import android.graphics.SurfaceTexture
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.Matrix
import com.devyk.av.camera_recorder.callback.IRenderer
import com.devyk.av.rtmp.library.R
import com.devyk.av.rtmp.library.camera.ShaderHelper
import com.devyk.av.rtmp.library.camera.Watermark
import com.devyk.av.rtmp.library.utils.LogHelper
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer


/**
 * <pre>
 *     author  : devyk on 2020-07-07 22:49
 *     blog    : https://juejin.im/user/578259398ac2470061f3a3fb/posts
 *     github  : https://github.com/yangkun19921001
 *     mailbox : yang1001yk@gmail.com
 *     desc    : This is CameraRenderer
 * </pre>
 */
public class CameraRenderer(context: Context) : IRenderer {

    private var TAG = this.javaClass.simpleName

    /**
     * 顶点坐标
     */
    private val mVertexData: FloatArray = floatArrayOf(
        -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f
    )

    /**
     *  纹理坐标(渲染到 FBO 中)
     */
    private val mFragmentData: FloatArray = floatArrayOf(
        // 正常纹理坐标
        0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f
    )

    /**
     * 定义一个上下文变量
     */
    private var mContext: Context = context

    /**
     * 执行着色器代码的程序
     */
    private var program = 0

    /**
     * 顶点索引
     */
    private var vPosition = 0

    /**
     * 纹理索引
     */
    private var fPosition = 0

    /**
     * 绘制的纹理 ID, 该纹理id绑定了fbo,在该纹理上绘制就是绘制到fbo中
     */
    private var mTextureIDBindWithFBO = 0

    /**
     * 使用 VBO
     * 概念:
     * - 不使用VBO时，我们每次绘制（ glDrawArrays ）图形时都是从本地内存处获取顶点数据然后传输给OpenGL来绘制，这样就会频繁的操作CPU->GPU增大开销，从而降低效率。
     * - 使用VBO，我们就能把顶点数据缓存到GPU开辟的一段内存中，然后使用时不必再从本地获取，而是直接从显存中获取，这样就能提升绘制的效率。
     *
     *
     */
    private var mVBOId = 0

    /**
     *  FBO 概念：
     * 为什么要用FBO?
     * - 当我们需要对纹理进行多次渲染采样时，而这些渲染采样是不需要展示给用户看的，所以我们就可以用一个单独的缓冲对象（离屏渲染）来存储我们的这几次渲染采样的结果，等处理完后才显示到窗口上。
     *
     * 优势
     * - 提高渲染效率，避免闪屏，可以很方便的实现纹理共享等。
     */
    private var mFBOId = 0


    /**
     * 矩阵值:正交投影
     */
    private var uMatrix = 0

    /**
     * 为顶点坐标分配 native 地址空间
     */
    private var mVertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(mVertexData.size * 4).order(ByteOrder.nativeOrder()) //大内存在前面，字节对齐
            .asFloatBuffer().put(mVertexData)

    /**
     * 为片元坐标分配 native 地址空间
     */
    private var mFragmentBuffer =
        ByteBuffer.allocateDirect(mFragmentData.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().put(mFragmentData)

    /**
     * FBO renderer
     */
    // 创建 FBORender
    private var mDrawCameraCaptureDataTextureToScreenRenderer: DrawCameraCaptureDataTextureToScreenRenderer =
        DrawCameraCaptureDataTextureToScreenRenderer(mContext)

    /**
     * bitmap 纹理 ID
     */
    private var mCameraPreviewTextureId = 0

    /**
     * 设置 FBO 离屏渲染的 size
     */
    private var mWidth = 1080
    private var mHeight = 1920

    private var mScreenWidth = 1080
    private var mScreenHeight = 1920


    @Volatile
    private var isTexture = true
    private var switchCameraListener: OnSwitchCameraListener? = null

    /**
     * 做矩阵方向变换的
     */
    private val mMatrix = FloatArray(16)

    private var mRendererListener: OnRendererListener? = null

    init {
        // 指向第一个索引，相当于 C 里面的第一个内存地址
        mVertexBuffer.position(0)
        mFragmentBuffer.position(0)
    }


    override fun onSurfaceCreate(width: Int, height: Int) {
        mScreenWidth = mContext.resources.displayMetrics.widthPixels
        mScreenHeight = mContext.resources.displayMetrics.heightPixels
        this.mHeight = height
        this.mWidth = width
        mDrawCameraCaptureDataTextureToScreenRenderer.onSurfaceCreate(width, height)

        // 1. 获取顶点/片元源代码资源
        val vertexSource = ShaderHelper.getRawShaderResource(mContext, R.raw.vertex_shader_matrix)
        val fragmentSource =
            ShaderHelper.getRawShaderResource(mContext, R.raw.fragment_shader_camera)

        // 2. 为顶点和片元创建一个执行程序
        program = ShaderHelper.createProgram(vertexSource, fragmentSource)

        // 3. 拿到顶点/片元 源代码中的索引位置
        vPosition = GLES20.glGetAttribLocation(program, "v_Position")
        fPosition = GLES20.glGetAttribLocation(program, "f_Position")
        uMatrix = GLES20.glGetUniformLocation(program, "u_Matrix")

        createVBOAndFBOForCapture()
        createSurfaceTextureForCameraPreview()
    }

    private fun createSurfaceTextureForCameraPreview() {
        // 7. 创建用于 Camera 预览的纹理
        val textureIdsEOS = IntArray(1)
        GLES20.glGenTextures(1, textureIdsEOS, 0)
        mCameraPreviewTextureId = textureIdsEOS[0]
        // 绑定相机纹理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mCameraPreviewTextureId)
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT
        )
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT
        )
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR
        )
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR
        )

        // 创建一个 SurfaceTexture 提供给 Camera, 相机会将预览数据绘制在这个纹理上
        val surfaceTexture = SurfaceTexture(mCameraPreviewTextureId)
        mRendererListener?.onCreate(surfaceTexture, mTextureIDBindWithFBO)

        // 解绑相机纹理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
    }

    private fun createVBOAndFBOForCapture() {
        // 这里需要牢牢记住，openGL的状态机制，前面绑定了某个资源，后续的操作对象都是该资源
        // 4. 生成一个 VBO
        val vbo = IntArray(1)
        GLES20.glGenBuffers(1, vbo, 0)
        mVBOId = vbo[0]
        // 4.1 绑定 VBO
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVBOId)
        // 4.2 分配 VBO 需要的缓存大小
        GLES20.glBufferData(
            GLES20.GL_ARRAY_BUFFER,
            mVertexData.size * 4 + mFragmentData.size * 4,
            null,
            GLES20.GL_STATIC_DRAW
        )
        // 4.3 将顶点和片元数据都保存到VBO中（为 VBO 设置顶点、片元数据的值）
        GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, mVertexData.size * 4, mVertexBuffer)
        GLES20.glBufferSubData(
            GLES20.GL_ARRAY_BUFFER, mVertexData.size * 4, mFragmentData.size * 4, mFragmentBuffer
        )
        // 4.4 解绑 VBO
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)

        // 5. 生成一个纹理 ID
        val textureIds = IntArray(1)
        GLES20.glGenTextures(1, textureIds, 0)
        mTextureIDBindWithFBO = textureIds[0]
        // 5.1. 指定将要绘制的纹理对象并且传递给对应的片元着色器中
        // GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        // 绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDBindWithFBO)

        // 5.2. 设置环绕和过滤方式
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)

        // 6. 生成一个 FBO
        val FBO = IntArray(1)
        GLES20.glGenFramebuffers(1, FBO, 0)
        mFBOId = FBO[0]
        // 6.1 绑定 FBO
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFBOId)

        // 6.2 设置 FBO 分配的内存大小
        GLES20.glTexImage2D(
            GLES20.GL_TEXTURE_2D,
            0,
            GLES20.GL_RGBA,
            mWidth,
            mHeight,
            0,
            GLES20.GL_RGBA,
            GLES20.GL_UNSIGNED_BYTE,
            null
        )

        // 6.3 将FBO渲染到指定纹理上
        // GLES20.glFramebufferTexture2D 是 OpenGL ES 2.0 中用于将一个纹理附加到帧缓冲区对象（Framebuffer Object, FBO）的特定颜色缓冲区的函数。
        // 这样做的目的是将渲染输出定向到该纹理，而不是默认的屏幕帧缓冲区。这种技术通常用于离屏渲染（off-screen rendering），例如进行后处理效果、纹理映射、阴影映射等。
        GLES20.glFramebufferTexture2D(
            GLES20.GL_FRAMEBUFFER,
            GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D,
            mTextureIDBindWithFBO,
            0
        )

        // 6.4 检查 FBO 是否绑定成功
        if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) {
            LogHelper.e(TAG, "FBO bind error")
        } else {
            LogHelper.e(TAG, "FBO bind success")
        }

        // 6.5 解绑 FBO
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)

        // 解绑纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
    }

    override fun onSurfaceChange(width: Int, height: Int) {
        //指定渲染框口的大小
        this.mHeight = height
        this.mWidth = width
    }


    override fun onDraw() {
        if (!isTexture) {
            Thread.sleep(300)
            isTexture = switchCameraListener?.onChange()!!
            if (!isTexture) return
        }

        // 将纹理图像更新为图像流中的最新帧。此方法只能在拥有该纹理的 OpenGL ES 上下文在调用线程上处于当前状态时调用。
        // 它将隐式地将其纹理绑定到 GL_TEXTURE_EXTERNAL_OES 纹理目标。
        // 也就是说，将会把相机最新的纹理，更新到 fragment_shader_camera
        // 代码中的 uniform samplerExternalOES sTexture; 对象上去。
        // 紧接着，下面的 renderToFBO 方法将会运行 shader 程序，并将结果渲染到FBO中，
        // 最后因为 mTextureIDBindWithFBO 绑定了FBO, 最后的绘制结果将会到 mTextureIDBindWithFBO 中。
        mRendererListener?.onDraw()

        // 将相机采集到的数据渲染到FBO中
        renderToFBO()

        // 使用绑定FBO的纹理id来绘制到屏幕上
        renderToScreen()
    }



    // 前面已经通过 mRendererListener?.onDraw() (对应执行 CameraHolder.instance().updateTexImage() )
    // 将相机流中最新的帧保存到 opengl 环境中在 fragment shader 代码中定义的 samplerExternalOES sTexture 中
    // 然后这里的作用就是开始运行程序，将 sTexture 中的图像绘制到FBO中，因为 mTextureIDBindWithFBO 绑定了 FBO,
    // 因此，最后相机采集的图像也对应保存到 mTextureIDBindWithFBO 纹理中去了。
    private fun renderToFBO() {
        // 相当于清屏
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

        // 1. 使用顶点和片元创建出来的执行程序
        GLES20.glUseProgram(program)
        GLES20.glViewport(0, 0, mScreenWidth, mScreenHeight)
        GLES20.glUniformMatrix4fv(uMatrix, 1, false, mMatrix, 0)

        // 2. 绑定 FBO (因为这里绑定了FBO，所以，后续的绘制都是绘制到FBO上，直到renderToFBO函数结束，解绑了FBO)
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFBOId)

        // 3. 绑定 VBO
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVBOId)

        // 4. 设置顶点坐标
        GLES20.glEnableVertexAttribArray(vPosition)
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, 0)

        // 5. 设置纹理坐标
        GLES20.glEnableVertexAttribArray(fPosition)
        GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8, mVertexData.size * 4)

        // 6. 开始绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        // 7. 解绑
        // 解绑 纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        // 解绑 VBO
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)
        // 解绑 FBO
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
    }

    // 将上面采集到的图像绘制到屏幕上(采集的结果在 mTextureIDBindWithFBO 中)
    private fun renderToScreen() {
        mDrawCameraCaptureDataTextureToScreenRenderer.onSurfaceChange(mWidth, mHeight)
        // 使用绑定FBO的纹理id来作为渲染到屏幕上的程序的输入纹理
        mDrawCameraCaptureDataTextureToScreenRenderer.onDraw(mTextureIDBindWithFBO)
    }

    /**
     * 设置水印
     */
    public fun setWatermark(watermark: Watermark) {
        mDrawCameraCaptureDataTextureToScreenRenderer.setWatermark(watermark)
    }


    public interface OnRendererListener {
        fun onCreate(cameraTextureId: Int, textureID: Int)
        fun onCreate(surfaceTexture: SurfaceTexture, textureID: Int)
        fun onDraw()
    }

    public fun setOnRendererListener(listener: OnRendererListener) {
        this.mRendererListener = listener
    }

    /**
     * 设置方向
     */
    fun setAngle(angle: Int, x: Int, y: Int, z: Int) {
        Matrix.rotateM(mMatrix, 0, angle.toFloat(), x.toFloat(), y.toFloat(), z.toFloat())
    }

    fun resetMatrix() {
        Matrix.setIdentityM(mMatrix, 0)
    }

    fun switchCamera(listener: OnSwitchCameraListener) {
        isTexture = false
        this.switchCameraListener = listener

    }

    public interface OnSwitchCameraListener {
        fun onChange(): Boolean
    }
}